#region License
//-----------------------------------------------------------------------------
// Copyright (c) 2008, Aaron MacDougall, Daniel Jeffery
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
//   this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
//
// * Neither the name of Aaron MacDougall or Daniel Jeffery nor the names of its contributors may
//   be used to endorse or promote products derived from this software without
//   specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace Agro2D
{
    public class SpriteManager
    {
        protected static uint MAX_LAYERS = 12;

        List<Sprite>[] drawableObjects;
        List<Sprite> invisibleObjects;
        List<Sprite> collisionList;

        bool inLevelEditor = false;
        bool showDebugVertices = false;

        /// <summary>
        /// Constructor.
        /// </summary>
        public SpriteManager()
        {
            this.drawableObjects = new List<Sprite>[MAX_LAYERS];
            for (int i = 0; i < MAX_LAYERS; i++)
                this.drawableObjects[i] = new List<Sprite>(20);
            this.invisibleObjects = new List<Sprite>(40);
            this.collisionList = new List<Sprite>(40);
        }

        /// <summary>
        /// Add drawable sprite.
        /// </summary>
        /// <param name="sprite">sprite to be drawn</param>
        /// <param name="layerNum">Layer number to add to. High layers are drawn above. Maximum 8 layers.</param>
        public void AddDrawable(Sprite sprite, uint layerNum)
        {
            Debug.Assert(layerNum >= 0 && layerNum < MAX_LAYERS, "Layer number given is higher than the number of layers or less than 0");
            Debug.Assert(!this.drawableObjects[layerNum].Contains(sprite), "Layer already contains the current sprite");

            this.drawableObjects[layerNum].Add(sprite);
            this.collisionList.Add(sprite);
            sprite.Deleted = false;
        }

        /// <summary>
        /// Add sprite that isn't to be drawn, just updated every frame.
        /// </summary>
        /// <param name="sprite">Sprite to be added to SpriteManager</param>
        public void AddInvisible(Sprite sprite)
        {
            Debug.Assert(!this.invisibleObjects.Contains(sprite));

            this.invisibleObjects.Add(sprite);
            this.collisionList.Add(sprite);
        }

        /// <summary>
        /// Remove a sprite from the drawable set of sprites.
        /// </summary>
        /// <param name="sprite">sprite to remove</param>
        /// <param name="layerNum">The layer that the sprite resides on</param>
        public void RemoveDrawable(Sprite sprite, uint layerNum)
        {
            sprite.Deleted = true;
        }

        /// <summary>
        /// Remove a sprite from the invisible set of sprites.
        /// </summary>
        /// <param name="sprite">sprite to remove</param>
        public void RemoveInvisible(Sprite sprite)
        {
            sprite.Deleted = true;
        }

        /// <summary>
        /// Gets a sprite from the loaded sprites.
        /// </summary>
        /// <param name="name">The name of the sprite</param>
        /// <returns>The desired Sprite if found, otherwise null</returns>
        public Sprite GetSprite(string name)
        {
            Sprite getSprite = null;
            foreach (Sprite sprite in this.collisionList)
            {
                if(sprite.Name == name)
                {
                    getSprite = sprite;
                    break;
                }                
            }

            return getSprite;
        }

        /// <summary>
        /// Array of lists of Drawable objects
        /// </summary>
        /// <returns></returns>
        public List<Sprite>[] GetDrawableObjects()
        {
            return drawableObjects;
        }

        public int NumDrawableObjects(int layer)
        {
            return drawableObjects[layer].Count;
        }

        public Sprite GetDrawableObject(int layer, int i)
        {
            return drawableObjects[layer][i];
        }

        /// <summary>
        /// List of Invisible objects
        /// </summary>
        /// <returns></returns>
        public List<Sprite> GetInvisibleObjects()
        {
            return collisionList;
        }

        /// <summary>
        /// List of Collidable objects, all objects are added to this list
        /// </summary>
        /// <returns></returns>
        public List<Sprite> CollisionList()
        {
            return collisionList;
        }

        public int NumCollisionObjects()
        {
            return collisionList.Count;
        }

        public Sprite GetCollidableObject(int i)
        {
            return collisionList[i];
        }

        /// <summary>
        /// Clear all types of loaded sprite.
        /// </summary>
        public virtual void Clear()
        {
            for (int i = 0; i < MAX_LAYERS; i++)
                this.drawableObjects[i].Clear();

            this.invisibleObjects.Clear();
            this.collisionList.Clear();
        }

        /// <summary>
        /// Update SpriteManager and all loaded sprites.
        /// </summary>
        /// <param name="gameTime">Game time</param>
        public virtual void Update(GameTime gameTime)
        {           
            // Update all sprites
            for (int i = 0; i < MAX_LAYERS; i++)
            {
                List<Sprite> removed = new List<Sprite>();

                Sprite item;
                for(int j = 0; j < this.drawableObjects[i].Count; ++j)
                {
                    item = drawableObjects[i][j];
                    if(item.Deleted == true)
                    {
                        // Move if requested.
                        if(item.NewLayerNum != 0 && item.NewLayerNum != 99
                            && item.NewLayerNum != item.LayerNum
                            && item.DeleteLayerNum == item.LayerNum)
                        {
                            if(item.Drawable)
                                this.AddDrawable(item, item.NewLayerNum);
                            else
                                this.AddInvisible(item);
                            item.LayerNum = item.NewLayerNum;
                            item.NewLayerNum = 99; 
                        }                       

                        removed.Add(item);
                    }
                }


                foreach(Sprite removeSprite in removed)
                {
                    drawableObjects[i].Remove(removeSprite);
                    this.collisionList.Remove(removeSprite);
                    removeSprite.Deleted = false;
                }

                
                for(int j = 0; j < this.drawableObjects[i].Count; ++j)
                {
                    item = this.drawableObjects[i][j];
                    item.Update(gameTime);
                }
            }

            Sprite sprite;
            //RemoveItems<Sprite>.Execute(invisibleObjects, delegate(Sprite item) { return item.Deleted == true; }); 
            for(int j = 0; j < this.invisibleObjects.Count; ++j)
            {
                sprite = this.invisibleObjects[j];
                sprite.Update(gameTime);
            }
                
        }

        /// <summary>
        /// Draw all drawable sprites. Doesn't draw sprites that are out of the viewport.
        /// </summary>
        /// <param name="spriteBatch">SpriteBatch to draw with</param>
        /// <param name="viewport">Viewport to draw sprites in</param>
        public virtual void Draw(SpriteBatch spriteBatch, ref Rectangle viewport, float scale)
        {
            Sprite sprite;
            for (uint i = 0; i < MAX_LAYERS; i++)
            {
                
                for(int j = 0; j < this.drawableObjects[i].Count; ++j)
                {
                    sprite = this.drawableObjects[i][j];
                    //if(sprite.Cull && !InLevelEditor)
                    //{
                    //    Rectangle srcRect;
                    //    Rectangle viewPortRect;

                    //    srcRect = sprite.SpriteProperties.SourceRectangle;

                    //    // Need to take rotated source rectangle into account.
                    //    if(sprite.SpriteProperties.Rotation == 0)
                    //    {
                    //        srcRect.Width *= (int)sprite.SpriteProperties.Scale;
                    //        srcRect.Height *= (int)sprite.SpriteProperties.Scale;
                    //    }
                    //    else
                    //    {
                    //        RotateRectangle(ref srcRect, sprite.SpriteProperties.Rotation);

                    //        srcRect.Width *= (int)sprite.SpriteProperties.Scale;
                    //        srcRect.Height *= (int)sprite.SpriteProperties.Scale;
                    //    }

                    //    // Scaled dimensions, distance from centre.
                    //    int distX, distY = 0;
                    //    distX = (int)MathHelper.Distance(sprite.SpriteProperties.SourceRectangle.Width / 2, srcRect.Width / 2);
                    //    distY = (int)MathHelper.Distance(sprite.SpriteProperties.SourceRectangle.Height / 2, srcRect.Height / 2);

                    //    srcRect.X = (int)sprite.SpriteProperties.Position.X + distX;
                    //    srcRect.Y = (int)sprite.SpriteProperties.Position.Y - distY;

                    //    viewPortRect = viewport;
                    //    viewPortRect.X = (viewport.Width - viewport.X) - viewport.Width;
                    //    viewPortRect.Y = (viewport.Height - viewport.Y) - viewport.Height;

                    //    // Test if sprite is in viewport.
                    //    bool inViewport;
                    //    viewPortRect.Intersects(ref srcRect, out inViewport);
                    //    if(inViewport)
                    //        sprite.Draw(spriteBatch, viewport);
                    //}
                    //else
                        sprite.Draw(spriteBatch, ref viewport, scale);
                }
            }
            
        }

        public void RotateRectangle(ref Rectangle rect, float rot)
        {
            int oX = rect.Width / 2;
            int oY = rect.Height / 2;

            double sinVal = Math.Sin(rot);
            double cosVal = Math.Cos(rot);

            // Top left.
            int x = rect.Left - oX;
            int y = rect.Top - oY;

            int x1 = (int)(x * cosVal - y * sinVal);
            int y1 = (int)(y * cosVal + x * sinVal);

            // Top right.
            x = rect.Right - oX;
            y = rect.Top - oY;

            int x2 = (int)(x * cosVal - y * sinVal);
            int y2 = (int)(y * cosVal + x * sinVal);

            // Bottom left.
            x = rect.Left - oX;
            y = rect.Bottom - oY;

            int x3 = (int)(x * cosVal - y * sinVal);
            int y3 = (int)(y * cosVal + x * sinVal);

            // Bottom right.
            x = rect.Right - oX;
            y = rect.Bottom - oY;

            int x4 = (int)(x * cosVal - y * sinVal);
            int y4 = (int)(y * cosVal + x * sinVal);

            x1 += oX; y1 += oY;
            x2 += oX; y2 += oY;
            x3 += oX; y3 += oY;
            x4 += oX; y4 += oY;

            int maxX = (int)MathHelper.Max(MathHelper.Max(x1, x2), MathHelper.Max(x3, x4));
            int minX = (int)MathHelper.Min(MathHelper.Min(x1, x2), MathHelper.Min(x3, x4));
            int width = (int)MathHelper.Distance(minX, maxX);

            int maxY = (int)MathHelper.Max(MathHelper.Max(y1, y2), MathHelper.Max(y3, y4));
            int minY = (int)MathHelper.Min(MathHelper.Min(y1, y2), MathHelper.Min(y3, y4));
            int height = (int)MathHelper.Distance(minY, maxY);

            rect.Width = width;
            rect.Height = height;
        }

        /// <summary>
        /// Returns with in levelEditor or not
        /// </summary>
        public bool InLevelEditor
        {
            get { return inLevelEditor; }
            set { inLevelEditor = value; }
        }

        public bool ShowDebugVertices
        {
            get { return showDebugVertices; }
            set { showDebugVertices = value; }
        }

        /// <summary>
        /// Check collision for sprite against all loaded sprites, including 
        /// invisible sprites.
        /// </summary>
        /// <param name="spriteIn">Sprite to test</param>
        /// <returns>true if there was a collision, false if not</returns>
        CollisionProperties spriteCollProps;
        CollisionProperties spriteInCollProps;
        Sprite sprite;
        public bool CheckCollision(Sprite spriteIn)
        {
            bool collision = false; // Collision occured?
            
            for(int j = 0; j < this.collisionList.Count; ++j)
            {
                sprite = this.collisionList[j];

                if(sprite == spriteIn)
                    continue;

                spriteCollProps = sprite.CollisionProperties;
                spriteInCollProps = spriteIn.CollisionProperties;

                // Mask CollisionGroups with CollidesWith
                if(((spriteCollProps.CollisionGroups &
                    spriteInCollProps.CollidesWith) == CollisionGroups.None) &
                    ((spriteInCollProps.CollisionGroups &
                    spriteCollProps.CollidesWith) == CollisionGroups.None)
                    )
                {
                    continue;
                }

                if (CollisionManager.Instance.CheckCollision(spriteIn, sprite))
                {
                    //// Sprite handlers.
                    //bool spriteInCollision = spriteIn.OnCollision(sprite);
                    //bool spriteCollision = sprite.OnCollision(spriteIn);

                    //if(spriteInCollision || spriteCollision)
                    //    collision = true;
                    // Sprite collision callbacks. NB: Doesn't override sprite handlers.
                    if(spriteIn.CollisionHandler != null)
                        if(spriteIn.CollisionHandler(spriteIn, sprite)) collision = true;
                    if(sprite.CollisionHandler != null)
                        if(spriteIn.CollisionHandler(sprite, spriteIn)) collision = true;
                }
            }
            return collision;
        }
    }
}